<?php

/**
 * Copyright 2011 Facebook, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
class PHPSDKTestCase extends PHPUnit_Framework_TestCase {

    const APP_ID = '117743971608120';
    const SECRET = '9c8ea2071859659bea1246d33a9207cf';
    const MIGRATED_APP_ID = '174236045938435';
    const MIGRATED_SECRET = '0073dce2d95c4a5c2922d1827ea0cca6';
    const TEST_USER = 499834690;
    const TEST_USER_2 = 499835484;

    private static $kExpiredAccessToken = 'AAABrFmeaJjgBAIshbq5ZBqZBICsmveZCZBi6O4w9HSTkFI73VMtmkL9jLuWsZBZC9QMHvJFtSulZAqonZBRIByzGooCZC8DWr0t1M4BL9FARdQwPWPnIqCiFQ';

    private static function kValidSignedRequest($id = self::TEST_USER, $oauth_token = null) {
        $facebook = new FBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        return $facebook->publicMakeSignedRequest(
                        array(
                            'user_id' => $id,
                            'oauth_token' => $oauth_token
                        )
        );
    }

    private static function kNonTosedSignedRequest() {
        $facebook = new FBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        return $facebook->publicMakeSignedRequest(array());
    }

    private static function kSignedRequestWithEmptyValue() {
        return '';
    }

    private static function kSignedRequestWithBogusSignature() {
        $facebook = new FBPublic(array(
            'appId' => self::APP_ID,
            'secret' => 'bogus',
        ));
        return $facebook->publicMakeSignedRequest(
                        array(
                            'algorithm' => 'HMAC-SHA256',
                        )
        );
    }

    private static function kSignedRequestWithWrongAlgo() {
        $facebook = new FBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $data['algorithm'] = 'foo';
        $json = json_encode($data);
        $b64 = $facebook->publicBase64UrlEncode($json);
        $raw_sig = hash_hmac('sha256', $b64, self::SECRET, $raw = true);
        $sig = $facebook->publicBase64UrlEncode($raw_sig);
        return $sig . '.' . $b64;
    }

    public function testConstructor() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $this->assertEquals($facebook->getAppId(), self::APP_ID, 'Expect the App ID to be set.');
        $this->assertEquals($facebook->getAppSecret(), self::SECRET, 'Expect the API secret to be set.');
    }

    public function testConstructorWithFileUpload() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'fileUpload' => true,
        ));
        $this->assertEquals($facebook->getAppId(), self::APP_ID, 'Expect the App ID to be set.');
        $this->assertEquals($facebook->getAppSecret(), self::SECRET, 'Expect the API secret to be set.');
        $this->assertTrue($facebook->getFileUploadSupport(), 'Expect file upload support to be on.');
        // alias (depricated) for getFileUploadSupport -- test until removed
        $this->assertTrue($facebook->useFileUploadSupport(), 'Expect file upload support to be on.');
    }

    public function testSetAppId() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $facebook->setAppId('dummy');
        $this->assertEquals($facebook->getAppId(), 'dummy', 'Expect the App ID to be dummy.');
    }

    public function testSetAPISecret() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $facebook->setApiSecret('dummy');
        $this->assertEquals($facebook->getApiSecret(), 'dummy', 'Expect the API secret to be dummy.');
    }

    public function testSetAPPSecret() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $facebook->setAppSecret('dummy');
        $this->assertEquals($facebook->getAppSecret(), 'dummy', 'Expect the API secret to be dummy.');
    }

    public function testSetAccessToken() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $facebook->setAccessToken('saltydog');
        $this->assertEquals($facebook->getAccessToken(), 'saltydog', 'Expect installed access token to remain \'saltydog\'');
    }

    public function testSetFileUploadSupport() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $this->assertFalse($facebook->getFileUploadSupport(), 'Expect file upload support to be off.');
        // alias for getFileUploadSupport (depricated), testing until removed
        $this->assertFalse($facebook->useFileUploadSupport(), 'Expect file upload support to be off.');
        $facebook->setFileUploadSupport(true);
        $this->assertTrue($facebook->getFileUploadSupport(), 'Expect file upload support to be on.');
        // alias for getFileUploadSupport (depricated), testing until removed
        $this->assertTrue($facebook->useFileUploadSupport(), 'Expect file upload support to be on.');
    }

    public function testGetCurrentURL() {
        $facebook = new FBGetCurrentURLFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        // fake the HPHP $_SERVER globals
        $_SERVER['HTTP_HOST'] = 'www.test.com';
        $_SERVER['REQUEST_URI'] = '/unit-tests.php?one=one&two=two&three=three';
        $current_url = $facebook->publicGetCurrentUrl();
        $this->assertEquals(
                'http://www.test.com/unit-tests.php?one=one&two=two&three=three', $current_url, 'getCurrentUrl function is changing the current URL');

        // ensure structure of valueless GET params is retained (sometimes
        // an = sign was present, and sometimes it was not)
        // first test when equal signs are present
        $_SERVER['HTTP_HOST'] = 'www.test.com';
        $_SERVER['REQUEST_URI'] = '/unit-tests.php?one=&two=&three=';
        $current_url = $facebook->publicGetCurrentUrl();
        $this->assertEquals(
                'http://www.test.com/unit-tests.php?one=&two=&three=', $current_url, 'getCurrentUrl function is changing the current URL');

        // now confirm that
        $_SERVER['HTTP_HOST'] = 'www.test.com';
        $_SERVER['REQUEST_URI'] = '/unit-tests.php?one&two&three';
        $current_url = $facebook->publicGetCurrentUrl();
        $this->assertEquals(
                'http://www.test.com/unit-tests.php?one&two&three', $current_url, 'getCurrentUrl function is changing the current URL');
    }

    public function testGetLoginURL() {
        $facebook = new Facebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        // fake the HPHP $_SERVER globals
        $_SERVER['HTTP_HOST'] = 'www.test.com';
        $_SERVER['REQUEST_URI'] = '/unit-tests.php';
        $login_url = parse_url($facebook->getLoginUrl());
        $this->assertEquals($login_url['scheme'], 'https');
        $this->assertEquals($login_url['host'], 'www.facebook.com');
        $this->assertEquals($login_url['path'], '/dialog/oauth');
        $expected_login_params =
                array('client_id' => self::APP_ID,
                    'redirect_uri' => 'http://www.test.com/unit-tests.php');

        $query_map = array();
        parse_str($login_url['query'], $query_map);
        $this->assertIsSubset($expected_login_params, $query_map);
        // we don't know what the state is, but we know it's an md5 and should
        // be 32 characters long.
        $this->assertEquals(strlen($query_map['state']), $num_characters = 32);
    }

    public function testGetLoginURLWithExtraParams() {
        $facebook = new Facebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        // fake the HPHP $_SERVER globals
        $_SERVER['HTTP_HOST'] = 'www.test.com';
        $_SERVER['REQUEST_URI'] = '/unit-tests.php';
        $extra_params = array('scope' => 'email, sms',
            'nonsense' => 'nonsense');
        $login_url = parse_url($facebook->getLoginUrl($extra_params));
        $this->assertEquals($login_url['scheme'], 'https');
        $this->assertEquals($login_url['host'], 'www.facebook.com');
        $this->assertEquals($login_url['path'], '/dialog/oauth');
        $expected_login_params =
                array_merge(
                array('client_id' => self::APP_ID,
            'redirect_uri' => 'http://www.test.com/unit-tests.php'), $extra_params);
        $query_map = array();
        parse_str($login_url['query'], $query_map);
        $this->assertIsSubset($expected_login_params, $query_map);
        // we don't know what the state is, but we know it's an md5 and should
        // be 32 characters long.
        $this->assertEquals(strlen($query_map['state']), $num_characters = 32);
    }

    public function testGetLoginURLWithScopeParamsAsArray() {
        $facebook = new Facebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        // fake the HPHP $_SERVER globals
        $_SERVER['HTTP_HOST'] = 'www.test.com';
        $_SERVER['REQUEST_URI'] = '/unit-tests.php';
        $scope_params_as_array = array('email', 'sms', 'read_stream');
        $extra_params = array('scope' => $scope_params_as_array,
            'nonsense' => 'nonsense');
        $login_url = parse_url($facebook->getLoginUrl($extra_params));
        $this->assertEquals($login_url['scheme'], 'https');
        $this->assertEquals($login_url['host'], 'www.facebook.com');
        $this->assertEquals($login_url['path'], '/dialog/oauth');
        // expect api to flatten array params to comma separated list
        // should do the same here before asserting to make sure API is behaving
        // correctly;
        $extra_params['scope'] = implode(',', $scope_params_as_array);
        $expected_login_params =
                array_merge(
                array('client_id' => self::APP_ID,
            'redirect_uri' => 'http://www.test.com/unit-tests.php'), $extra_params);
        $query_map = array();
        parse_str($login_url['query'], $query_map);
        $this->assertIsSubset($expected_login_params, $query_map);
        // we don't know what the state is, but we know it's an md5 and should
        // be 32 characters long.
        $this->assertEquals(strlen($query_map['state']), $num_characters = 32);
    }

    public function testGetCodeWithValidCSRFState() {
        $facebook = new FBCode(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $facebook->setCSRFStateToken();
        $code = $_REQUEST['code'] = $this->generateMD5HashOfRandomValue();
        $_REQUEST['state'] = $facebook->getCSRFStateToken();
        $this->assertEquals($code, $facebook->publicGetCode(), 'Expect code to be pulled from $_REQUEST[\'code\']');
    }

    public function testGetCodeWithInvalidCSRFState() {
        $facebook = new FBCode(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $facebook->setCSRFStateToken();
        $code = $_REQUEST['code'] = $this->generateMD5HashOfRandomValue();
        $_REQUEST['state'] = $facebook->getCSRFStateToken() . 'forgery!!!';
        $this->assertFalse($facebook->publicGetCode(), 'Expect getCode to fail, CSRF state should not match.');
    }

    public function testGetCodeWithMissingCSRFState() {
        $facebook = new FBCode(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $code = $_REQUEST['code'] = $this->generateMD5HashOfRandomValue();
        // intentionally don't set CSRF token at all
        $this->assertFalse($facebook->publicGetCode(), 'Expect getCode to fail, CSRF state not sent back.');
    }

    public function testPersistentCSRFState() {
        $facebook = new FBCode(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $facebook->setCSRFStateToken();
        $code = $facebook->getCSRFStateToken();

        $facebook = new FBCode(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $this->assertEquals($code, $facebook->publicGetState(), 'Persisted CSRF state token not loaded correctly');
    }

    public function testPersistentCSRFStateWithSharedSession() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $facebook = new FBCode(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));
        $facebook->setCSRFStateToken();
        $code = $facebook->getCSRFStateToken();

        $facebook = new FBCode(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));

        $this->assertEquals($code, $facebook->publicGetState(), 'Persisted CSRF state token not loaded correctly with shared session');
    }

    public function testGetUserFromSignedRequest() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $_REQUEST['signed_request'] = self::kValidSignedRequest();
        $this->assertEquals('499834690', $facebook->getUser(), 'Failed to get user ID from a valid signed request.');
    }

    public function testSignedRequestRewrite() {
        $facebook = new FBRewrite(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $_REQUEST['signed_request'] = self::kValidSignedRequest(self::TEST_USER, 'Hello sweetie');

        $this->assertEquals(self::TEST_USER, $facebook->getUser(), 'Failed to get user ID from a valid signed request.');

        $this->assertEquals('Hello sweetie', $facebook->getAccessToken(), 'Failed to get access token from signed request');

        $facebook->uncache();

        $_REQUEST['signed_request'] = self::kValidSignedRequest(self::TEST_USER_2, 'spoilers');

        $this->assertEquals(self::TEST_USER_2, $facebook->getUser(), 'Failed to get user ID from a valid signed request.');

        $_REQUEST['signed_request'] = null;
        $facebook->uncacheSignedRequest();

        $this->assertNotEquals('Hello sweetie', $facebook->getAccessToken(), 'Failed to clear access token');
    }

    public function testGetSignedRequestFromCookie() {
        $facebook = new FBPublicCookie(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $_COOKIE[$facebook->publicGetSignedRequestCookieName()] =
                self::kValidSignedRequest();
        $this->assertNotNull($facebook->publicGetSignedRequest());
        $this->assertEquals('499834690', $facebook->getUser(), 'Failed to get user ID from a valid signed request.');
    }

    public function testGetSignedRequestWithIncorrectSignature() {
        $facebook = new FBPublicCookie(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $_COOKIE[$facebook->publicGetSignedRequestCookieName()] =
                self::kSignedRequestWithBogusSignature();
        $this->assertNull($facebook->publicGetSignedRequest());
    }

    public function testNonUserAccessToken() {
        $facebook = new FBAccessToken(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        // no cookies, and no request params, so no user or code,
        // so no user access token (even with cookie support)
        $this->assertEquals($facebook->publicGetApplicationAccessToken(), $facebook->getAccessToken(), 'Access token should be that for logged out users.');
    }

    public function testMissingMetadataCookie() {
        $fb = new FBPublicCookie(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $this->assertEmpty($fb->publicGetMetadataCookie());
    }

    public function testEmptyMetadataCookie() {
        $fb = new FBPublicCookie(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $_COOKIE[$fb->publicGetMetadataCookieName()] = '';
        $this->assertEmpty($fb->publicGetMetadataCookie());
    }

    public function testMetadataCookie() {
        $fb = new FBPublicCookie(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $key = 'foo';
        $val = '42';
        $_COOKIE[$fb->publicGetMetadataCookieName()] = "$key=$val";
        $this->assertEquals(array($key => $val), $fb->publicGetMetadataCookie());
    }

    public function testQuotedMetadataCookie() {
        $fb = new FBPublicCookie(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $key = 'foo';
        $val = '42';
        $_COOKIE[$fb->publicGetMetadataCookieName()] = "\"$key=$val\"";
        $this->assertEquals(array($key => $val), $fb->publicGetMetadataCookie());
    }

    public function testAPIForLoggedOutUsers() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $response = $facebook->api(array(
            'method' => 'fql.query',
            'query' => 'SELECT name FROM user WHERE uid=4',
        ));
        $this->assertEquals(count($response), 1, 'Expect one row back.');
        $this->assertEquals($response[0]['name'], 'Mark Zuckerberg', 'Expect the name back.');
    }

    public function testAPIWithBogusAccessToken() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $facebook->setAccessToken('this-is-not-really-an-access-token');
        // if we don't set an access token and there's no way to
        // get one, then the FQL query below works beautifully, handing
        // over Zuck's public data.  But if you specify a bogus access
        // token as I have right here, then the FQL query should fail.
        // We could return just Zuck's public data, but that wouldn't
        // advertise the issue that the access token is at worst broken
        // and at best expired.
        try {
            $response = $facebook->api(array(
                'method' => 'fql.query',
                'query' => 'SELECT name FROM profile WHERE id=4',
            ));
            $this->fail('Should not get here.');
        } catch (FacebookApiException $e) {
            $result = $e->getResult();
            $this->assertTrue(is_array($result), 'expect a result object');
            $this->assertEquals('190', $result['error_code'], 'expect code');
        }
    }

    public function testAPIGraphPublicData() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $response = $facebook->api('/jerry');
        $this->assertEquals(
                $response['id'], '214707', 'should get expected id.');
    }

    public function testGraphAPIWithBogusAccessToken() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $facebook->setAccessToken('this-is-not-really-an-access-token');
        try {
            $response = $facebook->api('/me');
            $this->fail('Should not get here.');
        } catch (FacebookApiException $e) {
            // means the server got the access token and didn't like it
            $msg = 'OAuthException: Invalid OAuth access token.';
            $this->assertEquals($msg, (string) $e, 'Expect the invalid OAuth token message.');
        }
    }

    public function testGraphAPIWithExpiredAccessToken() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $facebook->setAccessToken(self::$kExpiredAccessToken);
        try {
            $response = $facebook->api('/me');
            $this->fail('Should not get here.');
        } catch (FacebookApiException $e) {
            // means the server got the access token and didn't like it
            $error_msg_start = 'OAuthException: Error validating access token:';
            $this->assertTrue(strpos((string) $e, $error_msg_start) === 0, 'Expect the token validation error message.');
        }
    }

    public function testGraphAPIOAuthSpecError() {
        $facebook = new TransientFacebook(array(
            'appId' => self::MIGRATED_APP_ID,
            'secret' => self::MIGRATED_SECRET,
        ));

        try {
            $response = $facebook->api('/me', array(
                'client_id' => self::MIGRATED_APP_ID));

            $this->fail('Should not get here.');
        } catch (FacebookApiException $e) {
            // means the server got the access token
            $msg = 'invalid_request: An active access token must be used ' .
                    'to query information about the current user.';
            $this->assertEquals($msg, (string) $e, 'Expect the invalid session message.');
        }
    }

    public function testGraphAPIMethodOAuthSpecError() {
        $facebook = new TransientFacebook(array(
            'appId' => self::MIGRATED_APP_ID,
            'secret' => self::MIGRATED_SECRET,
        ));

        try {
            $response = $facebook->api('/daaku.shah', 'DELETE', array(
                'client_id' => self::MIGRATED_APP_ID));
            $this->fail('Should not get here.');
        } catch (FacebookApiException $e) {
            $this->assertEquals(strpos($e, 'invalid_request'), 0);
        }
    }

    public function testCurlFailure() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        if (!defined('CURLOPT_TIMEOUT_MS')) {
            // can't test it if we don't have millisecond timeouts
            return;
        }

        $exception = null;
        try {
            // we dont expect facebook will ever return in 1ms
            Facebook::$CURL_OPTS[CURLOPT_TIMEOUT_MS] = 50;
            $facebook->api('/naitik');
        } catch (FacebookApiException $e) {
            $exception = $e;
        }
        unset(Facebook::$CURL_OPTS[CURLOPT_TIMEOUT_MS]);
        if (!$exception) {
            $this->fail('no exception was thrown on timeout.');
        }

        $code = $exception->getCode();
        if ($code != CURLE_OPERATION_TIMEOUTED && $code != CURLE_COULDNT_CONNECT) {
            $this->fail("Expected curl error code 7 or 28 but got: $code");
        }
        $this->assertEquals('CurlException', $exception->getType(), 'expect type');
    }

    public function testGraphAPIWithOnlyParams() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));

        $response = $facebook->api('/jerry');
        $this->assertTrue(isset($response['id']), 'User ID should be public.');
        $this->assertTrue(isset($response['name']), 'User\'s name should be public.');
        $this->assertTrue(isset($response['first_name']), 'User\'s first name should be public.');
        $this->assertTrue(isset($response['last_name']), 'User\'s last name should be public.');
        $this->assertFalse(isset($response['work']), 'User\'s work history should only be available with ' .
                'a valid access token.');
        $this->assertFalse(isset($response['education']), 'User\'s education history should only be ' .
                'available with a valid access token.');
        $this->assertFalse(isset($response['verified']), 'User\'s verification status should only be ' .
                'available with a valid access token.');
    }

    public function testLoginURLDefaults() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $_SERVER['REQUEST_URI'] = '/examples';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $encodedUrl = rawurlencode('http://fbrell.com/examples');
        $this->assertNotNull(strpos($facebook->getLoginUrl(), $encodedUrl), 'Expect the current url to exist.');
    }

    public function testLoginURLDefaultsDropStateQueryParam() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $_SERVER['REQUEST_URI'] = '/examples?state=xx42xx';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $expectEncodedUrl = rawurlencode('http://fbrell.com/examples');
        $this->assertTrue(strpos($facebook->getLoginUrl(), $expectEncodedUrl) > -1, 'Expect the current url to exist.');
        $this->assertFalse(strpos($facebook->getLoginUrl(), 'xx42xx'), 'Expect the session param to be dropped.');
    }

    public function testLoginURLDefaultsDropCodeQueryParam() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $_SERVER['REQUEST_URI'] = '/examples?code=xx42xx';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $expectEncodedUrl = rawurlencode('http://fbrell.com/examples');
        $this->assertTrue(strpos($facebook->getLoginUrl(), $expectEncodedUrl) > -1, 'Expect the current url to exist.');
        $this->assertFalse(strpos($facebook->getLoginUrl(), 'xx42xx'), 'Expect the session param to be dropped.');
    }

    public function testLoginURLDefaultsDropSignedRequestParamButNotOthers() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $_SERVER['REQUEST_URI'] =
                '/examples?signed_request=xx42xx&do_not_drop=xx43xx';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $expectEncodedUrl = rawurlencode('http://fbrell.com/examples');
        $this->assertFalse(strpos($facebook->getLoginUrl(), 'xx42xx'), 'Expect the session param to be dropped.');
        $this->assertTrue(strpos($facebook->getLoginUrl(), 'xx43xx') > -1, 'Expect the do_not_drop param to exist.');
    }

    public function testLoginURLCustomNext() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $_SERVER['REQUEST_URI'] = '/examples';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $next = 'http://fbrell.com/custom';
        $loginUrl = $facebook->getLoginUrl(array(
            'redirect_uri' => $next,
            'cancel_url' => $next
        ));
        $currentEncodedUrl = rawurlencode('http://fbrell.com/examples');
        $expectedEncodedUrl = rawurlencode($next);
        $this->assertNotNull(strpos($loginUrl, $expectedEncodedUrl), 'Expect the custom url to exist.');
        $this->assertFalse(strpos($loginUrl, $currentEncodedUrl), 'Expect the current url to not exist.');
    }

    public function testLogoutURLDefaults() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $_SERVER['REQUEST_URI'] = '/examples';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $encodedUrl = rawurlencode('http://fbrell.com/examples');
        $this->assertNotNull(strpos($facebook->getLogoutUrl(), $encodedUrl), 'Expect the current url to exist.');
        $this->assertFalse(strpos($facebook->getLogoutUrl(), self::SECRET));
    }

    public function testLoginStatusURLDefaults() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $_SERVER['REQUEST_URI'] = '/examples';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $encodedUrl = rawurlencode('http://fbrell.com/examples');
        $this->assertNotNull(strpos($facebook->getLoginStatusUrl(), $encodedUrl), 'Expect the current url to exist.');
    }

    public function testLoginStatusURLCustom() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $_SERVER['REQUEST_URI'] = '/examples';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $encodedUrl1 = rawurlencode('http://fbrell.com/examples');
        $okUrl = 'http://fbrell.com/here1';
        $encodedUrl2 = rawurlencode($okUrl);
        $loginStatusUrl = $facebook->getLoginStatusUrl(array(
            'ok_session' => $okUrl,
        ));
        $this->assertNotNull(strpos($loginStatusUrl, $encodedUrl1), 'Expect the current url to exist.');
        $this->assertNotNull(strpos($loginStatusUrl, $encodedUrl2), 'Expect the custom url to exist.');
    }

    public function testNonDefaultPort() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com:8080';
        $_SERVER['REQUEST_URI'] = '/examples';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $encodedUrl = rawurlencode('http://fbrell.com:8080/examples');
        $this->assertNotNull(strpos($facebook->getLoginUrl(), $encodedUrl), 'Expect the current url to exist.');
    }

    public function testSecureCurrentUrl() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $_SERVER['REQUEST_URI'] = '/examples';
        $_SERVER['HTTPS'] = 'on';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $encodedUrl = rawurlencode('https://fbrell.com/examples');
        $this->assertNotNull(strpos($facebook->getLoginUrl(), $encodedUrl), 'Expect the current url to exist.');
    }

    public function testSecureCurrentUrlWithNonDefaultPort() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com:8080';
        $_SERVER['REQUEST_URI'] = '/examples';
        $_SERVER['HTTPS'] = 'on';
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $encodedUrl = rawurlencode('https://fbrell.com:8080/examples');
        $this->assertNotNull(strpos($facebook->getLoginUrl(), $encodedUrl), 'Expect the current url to exist.');
    }

    public function testBase64UrlEncode() {
        $input = 'Facebook rocks';
        $output = 'RmFjZWJvb2sgcm9ja3M';

        $this->assertEquals(FBPublic::publicBase64UrlDecode($output), $input);
    }

    public function testSignedToken() {
        $facebook = new FBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));
        $payload = $facebook->publicParseSignedRequest(self::kValidSignedRequest());
        $this->assertNotNull($payload, 'Expected token to parse');
        $this->assertEquals($facebook->getSignedRequest(), null);
        $_REQUEST['signed_request'] = self::kValidSignedRequest();
        $this->assertEquals($facebook->getSignedRequest(), $payload);
    }

    public function testNonTossedSignedtoken() {
        $facebook = new FBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));
        $payload = $facebook->publicParseSignedRequest(
                self::kNonTosedSignedRequest());
        $this->assertNotNull($payload, 'Expected token to parse');
        $this->assertNull($facebook->getSignedRequest());
        $_REQUEST['signed_request'] = self::kNonTosedSignedRequest();
        $sr = $facebook->getSignedRequest();
        $this->assertTrue(isset($sr['algorithm']));
    }

    public function testSignedRequestWithEmptyValue() {
        $fb = new FBPublicCookie(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));
        $_REQUEST['signed_request'] = self::kSignedRequestWithEmptyValue();
        $this->assertNull($fb->getSignedRequest());
        $_COOKIE[$fb->publicGetSignedRequestCookieName()] =
                self::kSignedRequestWithEmptyValue();
        $this->assertNull($fb->getSignedRequest());
    }

    public function testSignedRequestWithWrongAlgo() {
        $fb = new FBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));
        $payload = $fb->publicParseSignedRequest(
                self::kSignedRequestWithWrongAlgo());
        $this->assertNull($payload, 'Expected nothing back.');
    }

    public function testMakeAndParse() {
        $fb = new FBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));
        $data = array('foo' => 42);
        $sr = $fb->publicMakeSignedRequest($data);
        $decoded = $fb->publicParseSignedRequest($sr);
        $this->assertEquals($data['foo'], $decoded['foo']);
    }

    /**
     * @expectedException InvalidArgumentException
     */
    public function testMakeSignedRequestExpectsArray() {
        $fb = new FBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));
        $sr = $fb->publicMakeSignedRequest('');
    }

    public function testBundledCACert() {
        $facebook = new TransientFacebook(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));

        // use the bundled cert from the start
        Facebook::$CURL_OPTS[CURLOPT_CAINFO] =
                dirname(__FILE__) . '/../src/fb_ca_chain_bundle.crt';
        $response = $facebook->api('/naitik');

        unset(Facebook::$CURL_OPTS[CURLOPT_CAINFO]);
        $this->assertEquals(
                $response['id'], '5526183', 'should get expected id.');
    }

    public function testVideoUpload() {
        $facebook = new FBRecordURL(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));

        $facebook->api(array('method' => 'video.upload'));
        $this->assertContains('//api-video.', $facebook->getRequestedURL(), 'video.upload should go against api-video');
    }

    public function testVideoUploadGraph() {
        $facebook = new FBRecordURL(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));

        $facebook->api('/me/videos', 'POST');
        $this->assertContains('//graph-video.', $facebook->getRequestedURL(), '/me/videos should go against graph-video');
    }

    public function testGetUserAndAccessTokenFromSession() {
        $facebook = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));

        $facebook->publicSetPersistentData('access_token', self::$kExpiredAccessToken);
        $facebook->publicSetPersistentData('user_id', 12345);
        $this->assertEquals(self::$kExpiredAccessToken, $facebook->getAccessToken(), 'Get access token from persistent store.');
        $this->assertEquals('12345', $facebook->getUser(), 'Get user id from persistent store.');
    }

    public function testGetUserAndAccessTokenFromSignedRequestNotSession() {
        $facebook = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));

        $_REQUEST['signed_request'] = self::kValidSignedRequest();
        $facebook->publicSetPersistentData('user_id', 41572);
        $facebook->publicSetPersistentData('access_token', self::$kExpiredAccessToken);
        $this->assertNotEquals('41572', $facebook->getUser(), 'Got user from session instead of signed request.');
        $this->assertEquals('499834690', $facebook->getUser(), 'Failed to get correct user ID from signed request.');
        $this->assertNotEquals(
                self::$kExpiredAccessToken, $facebook->getAccessToken(), 'Got access token from session instead of signed request.');
        $this->assertNotEmpty(
                $facebook->getAccessToken(), 'Failed to extract an access token from the signed request.');
    }

    public function testGetUserWithoutCodeOrSignedRequestOrSession() {
        $facebook = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));

        // deliberately leave $_REQUEST and _$SESSION empty
        $this->assertEmpty($_REQUEST, 'GET, POST, and COOKIE params exist even though ' .
                'they should.  Test cannot succeed unless all of ' .
                '$_REQUEST is empty.');
        $this->assertEmpty($_SESSION, 'Session is carrying state and should not be.');
        $this->assertEmpty($facebook->getUser(), 'Got a user id, even without a signed request, ' .
                'access token, or session variable.');
        $this->assertEmpty($_SESSION, 'Session superglobal incorrectly populated by getUser.');
    }

    public function testGetAccessTokenUsingCodeInJsSdkCookie() {
        $code = 'code1';
        $access_token = 'at1';
        $methods_to_stub = array('getSignedRequest', 'getAccessTokenFromCode');
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('getSignedRequest')
                ->will($this->returnValue(array('code' => $code)));
        $stub
                ->expects($this->once())
                ->method('getAccessTokenFromCode')
                ->will($this->returnValueMap(array(array($code, '', $access_token))));
        $this->assertEquals($stub->getAccessToken(), $access_token);
    }

    public function testSignedRequestWithoutAuthClearsData() {
        $methods_to_stub = array('getSignedRequest', 'clearAllPersistentData');
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('getSignedRequest')
                ->will($this->returnValue(array('foo' => 1)));
        $stub
                ->expects($this->once())
                ->method('clearAllPersistentData');
        $this->assertEquals(self::APP_ID . '|' . self::SECRET, $stub->getAccessToken());
    }

    public function testInvalidCodeInSignedRequestWillClearData() {
        $code = 'code1';
        $methods_to_stub = array(
            'getSignedRequest',
            'getAccessTokenFromCode',
            'clearAllPersistentData',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('getSignedRequest')
                ->will($this->returnValue(array('code' => $code)));
        $stub
                ->expects($this->once())
                ->method('getAccessTokenFromCode')
                ->will($this->returnValue(null));
        $stub
                ->expects($this->once())
                ->method('clearAllPersistentData');
        $this->assertEquals(self::APP_ID . '|' . self::SECRET, $stub->getAccessToken());
    }

    public function testInvalidCodeWillClearData() {
        $code = 'code1';
        $methods_to_stub = array(
            'getCode',
            'getAccessTokenFromCode',
            'clearAllPersistentData',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('getCode')
                ->will($this->returnValue($code));
        $stub
                ->expects($this->once())
                ->method('getAccessTokenFromCode')
                ->will($this->returnValue(null));
        $stub
                ->expects($this->once())
                ->method('clearAllPersistentData');
        $this->assertEquals(self::APP_ID . '|' . self::SECRET, $stub->getAccessToken());
    }

    public function testValidCodeToToken() {
        $code = 'code1';
        $access_token = 'at1';
        $methods_to_stub = array(
            'getSignedRequest',
            'getCode',
            'getAccessTokenFromCode',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('getCode')
                ->will($this->returnValue($code));
        $stub
                ->expects($this->once())
                ->method('getAccessTokenFromCode')
                ->will($this->returnValueMap(array(array($code, null, $access_token))));
        $this->assertEquals($stub->getAccessToken(), $access_token);
    }

    public function testSignedRequestWithoutAuthClearsDataInAvailData() {
        $methods_to_stub = array('getSignedRequest', 'clearAllPersistentData');
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('getSignedRequest')
                ->will($this->returnValue(array('foo' => 1)));
        $stub
                ->expects($this->once())
                ->method('clearAllPersistentData');
        $this->assertEquals(0, $stub->getUser());
    }

    public function testFailedToGetUserFromAccessTokenClearsData() {
        $methods_to_stub = array(
            'getAccessToken',
            'getUserFromAccessToken',
            'clearAllPersistentData',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('getAccessToken')
                ->will($this->returnValue('at1'));
        $stub
                ->expects($this->once())
                ->method('getUserFromAccessToken');
        $stub
                ->expects($this->once())
                ->method('clearAllPersistentData');
        $this->assertEquals(0, $stub->getUser());
    }

    public function testUserFromAccessTokenIsStored() {
        $methods_to_stub = array(
            'getAccessToken',
            'getUserFromAccessToken',
            'setPersistentData',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $user = 42;
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('getAccessToken')
                ->will($this->returnValue('at1'));
        $stub
                ->expects($this->once())
                ->method('getUserFromAccessToken')
                ->will($this->returnValue($user));
        $stub
                ->expects($this->once())
                ->method('setPersistentData');
        $this->assertEquals($user, $stub->getUser());
    }

    public function testUserFromAccessTokenPullsID() {
        $methods_to_stub = array(
            'getAccessToken',
            'api',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $user = 42;
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('getAccessToken')
                ->will($this->returnValue('at1'));
        $stub
                ->expects($this->once())
                ->method('api')
                ->will($this->returnValue(array('id' => $user)));
        $this->assertEquals($user, $stub->getUser());
    }

    public function testUserFromAccessTokenResetsOnApiException() {
        $methods_to_stub = array(
            'getAccessToken',
            'clearAllPersistentData',
            'api',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('getAccessToken')
                ->will($this->returnValue('at1'));
        $stub
                ->expects($this->once())
                ->method('api')
                ->will($this->throwException(new FacebookApiException(false)));
        $stub
                ->expects($this->once())
                ->method('clearAllPersistentData');
        $this->assertEquals(0, $stub->getUser());
    }

    public function testEmptyCodeReturnsFalse() {
        $fb = new FBPublicGetAccessTokenFromCode(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));
        $this->assertFalse($fb->publicGetAccessTokenFromCode(''));
        $this->assertFalse($fb->publicGetAccessTokenFromCode(null));
        $this->assertFalse($fb->publicGetAccessTokenFromCode(false));
    }

    public function testNullRedirectURIUsesCurrentURL() {
        $methods_to_stub = array(
            '_oauthRequest',
            'getCurrentUrl',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $access_token = 'at1';
        $stub = $this->getMock(
                'FBPublicGetAccessTokenFromCode', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('_oauthRequest')
                ->will($this->returnValue("access_token=$access_token"));
        $stub
                ->expects($this->once())
                ->method('getCurrentUrl');
        $this->assertEquals(
                $access_token, $stub->publicGetAccessTokenFromCode('c'));
    }

    public function testNullRedirectURIAllowsEmptyStringForCookie() {
        $methods_to_stub = array(
            '_oauthRequest',
            'getCurrentUrl',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $access_token = 'at1';
        $stub = $this->getMock(
                'FBPublicGetAccessTokenFromCode', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('_oauthRequest')
                ->will($this->returnValue("access_token=$access_token"));
        $stub
                ->expects($this->never())
                ->method('getCurrentUrl');
        $this->assertEquals(
                $access_token, $stub->publicGetAccessTokenFromCode('c', ''));
    }

    public function testAPIExceptionDuringCodeExchangeIsIgnored() {
        $methods_to_stub = array(
            '_oauthRequest',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'FBPublicGetAccessTokenFromCode', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('_oauthRequest')
                ->will($this->throwException(new FacebookApiException(false)));
        $this->assertFalse($stub->publicGetAccessTokenFromCode('c', ''));
    }

    public function testEmptyResponseInCodeExchangeIsIgnored() {
        $methods_to_stub = array(
            '_oauthRequest',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'FBPublicGetAccessTokenFromCode', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('_oauthRequest')
                ->will($this->returnValue(''));
        $this->assertFalse($stub->publicGetAccessTokenFromCode('c', ''));
    }

    public function testExistingStateRestoredInConstructor() {
        $fb = new FBPublicState(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET
        ));
        $this->assertEquals(FBPublicState::STATE, $fb->publicGetState());
    }

    public function testMissingAccessTokenInCodeExchangeIsIgnored() {
        $methods_to_stub = array(
            '_oauthRequest',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'FBPublicGetAccessTokenFromCode', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('_oauthRequest')
                ->will($this->returnValue('foo=1'));
        $this->assertFalse($stub->publicGetAccessTokenFromCode('c', ''));
    }

    public function testExceptionConstructorWithErrorCode() {
        $code = 404;
        $e = new FacebookApiException(array('error_code' => $code));
        $this->assertEquals($code, $e->getCode());
    }

    public function testExceptionConstructorWithInvalidErrorCode() {
        $e = new FacebookApiException(array('error_code' => 'not an int'));
        $this->assertEquals(0, $e->getCode());
    }

    // this happens often despite the fact that it is useless
    public function testExceptionTypeFalse() {
        $e = new FacebookApiException(false);
        $this->assertEquals('Exception', $e->getType());
    }

    public function testExceptionTypeMixedDraft00() {
        $e = new FacebookApiException(array('error' => array('message' => 'foo')));
        $this->assertEquals('Exception', $e->getType());
    }

    public function testExceptionTypeDraft00() {
        $error = 'foo';
        $e = new FacebookApiException(
                array('error' => array('type' => $error, 'message' => 'hello world')));
        $this->assertEquals($error, $e->getType());
    }

    public function testExceptionTypeDraft10() {
        $error = 'foo';
        $e = new FacebookApiException(array('error' => $error));
        $this->assertEquals($error, $e->getType());
    }

    public function testExceptionTypeDefault() {
        $e = new FacebookApiException(array('error' => false));
        $this->assertEquals('Exception', $e->getType());
    }

    public function testExceptionToString() {
        $e = new FacebookApiException(array(
            'error_code' => 1,
            'error_description' => 'foo',
        ));
        $this->assertEquals('Exception: 1: foo', (string) $e);
    }

    public function testDestroyClearsCookie() {
        $fb = new FBPublicCookie(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $_COOKIE[$fb->publicGetSignedRequestCookieName()] = 'foo';
        $_COOKIE[$fb->publicGetMetadataCookieName()] = 'base_domain=fbrell.com';
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $fb->destroySession();
        $this->assertFalse(
                array_key_exists($fb->publicGetSignedRequestCookieName(), $_COOKIE));
    }

    public function testAuthExpireSessionDestroysSession() {
        $methods_to_stub = array(
            '_oauthRequest',
            'destroySession',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $key = 'foo';
        $val = 42;
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('_oauthRequest')
                ->will($this->returnValue("{\"$key\":$val}"));
        $stub
                ->expects($this->once())
                ->method('destroySession');
        $this->assertEquals(
                array($key => $val), $stub->api(array('method' => 'auth.expireSession'))
        );
    }

    public function testLowercaseAuthRevokeAuthDestroysSession() {
        $methods_to_stub = array(
            '_oauthRequest',
            'destroySession',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $key = 'foo';
        $val = 42;
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('_oauthRequest')
                ->will($this->returnValue("{\"$key\":$val}"));
        $stub
                ->expects($this->once())
                ->method('destroySession');
        $this->assertEquals(
                array($key => $val), $stub->api(array('method' => 'auth.revokeauthorization'))
        );
    }

    /**
     * @expectedException FacebookAPIException
     */
    public function testErrorCodeFromRestAPIThrowsException() {
        $methods_to_stub = array(
            '_oauthRequest',
        );
        $constructor_args = array(array(
                'appId' => self::APP_ID,
                'secret' => self::SECRET
        ));
        $stub = $this->getMock(
                'TransientFacebook', $methods_to_stub, $constructor_args);
        $stub
                ->expects($this->once())
                ->method('_oauthRequest')
                ->will($this->returnValue('{"error_code": 500}'));
        $stub->api(array('method' => 'foo'));
    }

    public function testJsonEncodeOfNonStringParams() {
        $foo = array(1, 2);
        $params = array(
            'method' => 'get',
            'foo' => $foo,
        );
        $fb = new FBRecordMakeRequest(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $fb->api('/naitik', $params);
        $requests = $fb->publicGetRequests();
        $this->assertEquals(json_encode($foo), $requests[0]['params']['foo']);
    }

    public function testSessionBackedFacebook() {
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $key = 'state';
        $val = 'foo';
        $fb->publicSetPersistentData($key, $val);
        $this->assertEquals(
                $val, $_SESSION[sprintf('fb_%s_%s', self::APP_ID, $key)]
        );
        $this->assertEquals(
                $val, $fb->publicGetPersistentData($key)
        );
    }

    public function testSessionBackedFacebookIgnoresUnsupportedKey() {
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $key = '--invalid--';
        $val = 'foo';
        $fb->publicSetPersistentData($key, $val);
        $this->assertFalse(
                array_key_exists(
                        sprintf('fb_%s_%s', self::APP_ID, $key), $_SESSION
                )
        );
        $this->assertFalse($fb->publicGetPersistentData($key));
    }

    public function testClearSessionBackedFacebook() {
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $key = 'state';
        $val = 'foo';
        $fb->publicSetPersistentData($key, $val);
        $this->assertEquals(
                $val, $_SESSION[sprintf('fb_%s_%s', self::APP_ID, $key)]
        );
        $this->assertEquals(
                $val, $fb->publicGetPersistentData($key)
        );
        $fb->publicClearPersistentData($key);
        $this->assertFalse(
                array_key_exists(
                        sprintf('fb_%s_%s', self::APP_ID, $key), $_SESSION
                )
        );
        $this->assertFalse($fb->publicGetPersistentData($key));
    }

    public function testSessionBackedFacebookIgnoresUnsupportedKeyInClear() {
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $key = '--invalid--';
        $val = 'foo';
        $session_var_name = sprintf('fb_%s_%s', self::APP_ID, $key);
        $_SESSION[$session_var_name] = $val;
        $fb->publicClearPersistentData($key);
        $this->assertTrue(array_key_exists($session_var_name, $_SESSION));
        $this->assertFalse($fb->publicGetPersistentData($key));
    }

    public function testClearAllSessionBackedFacebook() {
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $key = 'state';
        $val = 'foo';
        $session_var_name = sprintf('fb_%s_%s', self::APP_ID, $key);
        $fb->publicSetPersistentData($key, $val);
        $this->assertEquals($val, $_SESSION[$session_var_name]);
        $this->assertEquals($val, $fb->publicGetPersistentData($key));
        $fb->publicClearAllPersistentData();
        $this->assertFalse(array_key_exists($session_var_name, $_SESSION));
        $this->assertFalse($fb->publicGetPersistentData($key));
    }

    public function testSharedSessionBackedFacebook() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));
        $key = 'state';
        $val = 'foo';
        $session_var_name = sprintf(
                '%s_fb_%s_%s', $fb->publicGetSharedSessionID(), self::APP_ID, $key
        );
        $fb->publicSetPersistentData($key, $val);
        $this->assertEquals($val, $_SESSION[$session_var_name]);
        $this->assertEquals($val, $fb->publicGetPersistentData($key));
    }

    public function testSharedSessionBackedFacebookIgnoresUnsupportedKey() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));
        $key = '--invalid--';
        $val = 'foo';
        $session_var_name = sprintf(
                '%s_fb_%s_%s', $fb->publicGetSharedSessionID(), self::APP_ID, $key
        );
        $fb->publicSetPersistentData($key, $val);
        $this->assertFalse(array_key_exists($session_var_name, $_SESSION));
        $this->assertFalse($fb->publicGetPersistentData($key));
    }

    public function testSharedClearSessionBackedFacebook() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));
        $key = 'state';
        $val = 'foo';
        $session_var_name = sprintf(
                '%s_fb_%s_%s', $fb->publicGetSharedSessionID(), self::APP_ID, $key
        );
        $fb->publicSetPersistentData($key, $val);
        $this->assertEquals($val, $_SESSION[$session_var_name]);
        $this->assertEquals($val, $fb->publicGetPersistentData($key));
        $fb->publicClearPersistentData($key);
        $this->assertFalse(array_key_exists($session_var_name, $_SESSION));
        $this->assertFalse($fb->publicGetPersistentData($key));
    }

    public function testSharedSessionBackedFacebookIgnoresUnsupportedKeyInClear() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));
        $key = '--invalid--';
        $val = 'foo';
        $session_var_name = sprintf(
                '%s_fb_%s_%s', $fb->publicGetSharedSessionID(), self::APP_ID, $key
        );
        $_SESSION[$session_var_name] = $val;
        $fb->publicClearPersistentData($key);
        $this->assertTrue(array_key_exists($session_var_name, $_SESSION));
        $this->assertFalse($fb->publicGetPersistentData($key));
    }

    public function testSharedClearAllSessionBackedFacebook() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));
        $key = 'state';
        $val = 'foo';
        $session_var_name = sprintf(
                '%s_fb_%s_%s', $fb->publicGetSharedSessionID(), self::APP_ID, $key
        );
        $fb->publicSetPersistentData($key, $val);
        $this->assertEquals($val, $_SESSION[$session_var_name]);
        $this->assertEquals($val, $fb->publicGetPersistentData($key));
        $fb->publicClearAllPersistentData();
        $this->assertFalse(array_key_exists($session_var_name, $_SESSION));
        $this->assertFalse($fb->publicGetPersistentData($key));
    }

    public function testSharedSessionBackedFacebookIsRestored() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));
        $key = 'state';
        $val = 'foo';
        $shared_session_id = $fb->publicGetSharedSessionID();
        $session_var_name = sprintf(
                '%s_fb_%s_%s', $shared_session_id, self::APP_ID, $key
        );
        $fb->publicSetPersistentData($key, $val);
        $this->assertEquals($val, $_SESSION[$session_var_name]);
        $this->assertEquals($val, $fb->publicGetPersistentData($key));

        // check the new instance has the same data
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));
        $this->assertEquals(
                $shared_session_id, $fb->publicGetSharedSessionID()
        );
        $this->assertEquals($val, $fb->publicGetPersistentData($key));
    }

    public function testSharedSessionBackedFacebookIsNotRestoredWhenCorrupt() {
        $_SERVER['HTTP_HOST'] = 'fbrell.com';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));
        $key = 'state';
        $val = 'foo';
        $shared_session_id = $fb->publicGetSharedSessionID();
        $session_var_name = sprintf(
                '%s_fb_%s_%s', $shared_session_id, self::APP_ID, $key
        );
        $fb->publicSetPersistentData($key, $val);
        $this->assertEquals($val, $_SESSION[$session_var_name]);
        $this->assertEquals($val, $fb->publicGetPersistentData($key));

        // break the cookie
        $cookie_name = $fb->publicGetSharedSessionCookieName();
        $_COOKIE[$cookie_name] = substr($_COOKIE[$cookie_name], 1);

        // check the new instance does not have the data
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'sharedSession' => true,
        ));
        $this->assertFalse($fb->publicGetPersistentData($key));
        $this->assertNotEquals(
                $shared_session_id, $fb->publicGetSharedSessionID()
        );
    }

    public function testHttpHost() {
        $real = 'foo.com';
        $_SERVER['HTTP_HOST'] = $real;
        $_SERVER['HTTP_X_FORWARDED_HOST'] = 'evil.com';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $this->assertEquals($real, $fb->publicGetHttpHost());
    }

    public function testHttpProtocol() {
        $_SERVER['HTTPS'] = 'on';
        $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'http';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
        ));
        $this->assertEquals('https', $fb->publicGetHttpProtocol());
    }

    public function testHttpHostForwarded() {
        $real = 'foo.com';
        $_SERVER['HTTP_HOST'] = 'localhost';
        $_SERVER['HTTP_X_FORWARDED_HOST'] = $real;
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'trustForwarded' => true,
        ));
        $this->assertEquals($real, $fb->publicGetHttpHost());
    }

    public function testHttpProtocolForwarded() {
        $_SERVER['HTTPS'] = 'on';
        $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'http';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'trustForwarded' => true,
        ));
        $this->assertEquals('http', $fb->publicGetHttpProtocol());
    }

    public function testHttpProtocolForwardedSecure() {
        $_SERVER['HTTPS'] = 'on';
        $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'https';
        $fb = new PersistentFBPublic(array(
            'appId' => self::APP_ID,
            'secret' => self::SECRET,
            'trustForwarded' => true,
        ));
        $this->assertEquals('https', $fb->publicGetHttpProtocol());
    }

    /**
     * @dataProvider provideEndsWith
     */
    public function testEndsWith($big, $small, $result) {
        $this->assertEquals(
                $result, PersistentFBPublic::publicEndsWith($big, $small)
        );
    }

    public function provideEndsWith() {
        return array(
            array('', '', true),
            array('', 'a', false),
            array('a', '', true),
            array('a', 'b', false),
            array('a', 'a', true),
            array('aa', 'a', true),
            array('ab', 'a', false),
            array('ba', 'a', true),
        );
    }

    /**
     * @dataProvider provideIsAllowedDomain
     */
    public function testIsAllowedDomain($big, $small, $result) {
        $this->assertEquals(
                $result, PersistentFBPublic::publicIsAllowedDomain($big, $small)
        );
    }

    public function provideIsAllowedDomain() {
        return array(
            array('fbrell.com', 'fbrell.com', true),
            array('foo.fbrell.com', 'fbrell.com', true),
            array('foofbrell.com', 'fbrell.com', false),
            array('evil.com', 'fbrell.com', false),
            array('foo.fbrell.com', 'bar.fbrell.com', false),
        );
    }

    protected function generateMD5HashOfRandomValue() {
        return md5(uniqid(mt_rand(), true));
    }

    protected function setUp() {
        parent::setUp();
    }

    protected function tearDown() {
        $this->clearSuperGlobals();
        parent::tearDown();
    }

    protected function clearSuperGlobals() {
        unset($_SERVER['HTTPS']);
        unset($_SERVER['HTTP_HOST']);
        unset($_SERVER['REQUEST_URI']);
        $_SESSION = array();
        $_COOKIE = array();
        $_REQUEST = array();
        $_POST = array();
        $_GET = array();
        if (session_id()) {
            session_destroy();
        }
    }

    /**
     * Checks that the correct args are a subset of the returned obj
     * @param  array $correct The correct array values
     * @param  array $actual  The values in practice
     * @param  string $message to be shown on failure
     */
    protected function assertIsSubset($correct, $actual, $msg = '') {
        foreach ($correct as $key => $value) {
            $actual_value = $actual[$key];
            $newMsg = (strlen($msg) ? ($msg . ' ') : '') . 'Key: ' . $key;
            $this->assertEquals($value, $actual_value, $newMsg);
        }
    }

}

class TransientFacebook extends BaseFacebook {

    protected function setPersistentData($key, $value) {
        
    }

    protected function getPersistentData($key, $default = false) {
        return $default;
    }

    protected function clearPersistentData($key) {
        
    }

    protected function clearAllPersistentData() {
        
    }

}

class FBRecordURL extends TransientFacebook {

    private $url;

    protected function _oauthRequest($url, $params) {
        $this->url = $url;
    }

    public function getRequestedURL() {
        return $this->url;
    }

}

class FBRecordMakeRequest extends TransientFacebook {

    private $requests = array();

    protected function makeRequest($url, $params, $ch = null) {
        $this->requests[] = array(
            'url' => $url,
            'params' => $params,
        );
        return parent::makeRequest($url, $params, $ch);
    }

    public function publicGetRequests() {
        return $this->requests;
    }

}

class FBPublic extends TransientFacebook {

    public static function publicBase64UrlDecode($input) {
        return self::base64UrlDecode($input);
    }

    public static function publicBase64UrlEncode($input) {
        return self::base64UrlEncode($input);
    }

    public function publicParseSignedRequest($input) {
        return $this->parseSignedRequest($input);
    }

    public function publicMakeSignedRequest($data) {
        return $this->makeSignedRequest($data);
    }

}

class PersistentFBPublic extends Facebook {

    public function publicParseSignedRequest($input) {
        return $this->parseSignedRequest($input);
    }

    public function publicSetPersistentData($key, $value) {
        $this->setPersistentData($key, $value);
    }

    public function publicGetPersistentData($key, $default = false) {
        return $this->getPersistentData($key, $default);
    }

    public function publicClearPersistentData($key) {
        return $this->clearPersistentData($key);
    }

    public function publicClearAllPersistentData() {
        return $this->clearAllPersistentData();
    }

    public function publicGetSharedSessionID() {
        return $this->sharedSessionID;
    }

    public static function publicIsAllowedDomain($big, $small) {
        return self::isAllowedDomain($big, $small);
    }

    public static function publicEndsWith($big, $small) {
        return self::endsWith($big, $small);
    }

    public function publicGetSharedSessionCookieName() {
        return $this->getSharedSessionCookieName();
    }

    public function publicGetHttpHost() {
        return $this->getHttpHost();
    }

    public function publicGetHttpProtocol() {
        return $this->getHttpProtocol();
    }

}

class FBCode extends Facebook {

    public function publicGetCode() {
        return $this->getCode();
    }

    public function publicGetState() {
        return $this->state;
    }

    public function setCSRFStateToken() {
        $this->establishCSRFTokenState();
    }

    public function getCSRFStateToken() {
        return $this->getPersistentData('state');
    }

}

class FBAccessToken extends TransientFacebook {

    public function publicGetApplicationAccessToken() {
        return $this->getApplicationAccessToken();
    }

}

class FBGetCurrentURLFacebook extends TransientFacebook {

    public function publicGetCurrentUrl() {
        return $this->getCurrentUrl();
    }

}

class FBPublicCookie extends TransientFacebook {

    public function publicGetSignedRequest() {
        return $this->getSignedRequest();
    }

    public function publicGetSignedRequestCookieName() {
        return $this->getSignedRequestCookieName();
    }

    public function publicGetMetadataCookie() {
        return $this->getMetadataCookie();
    }

    public function publicGetMetadataCookieName() {
        return $this->getMetadataCookieName();
    }

}

class FBRewrite extends Facebook {

    public function uncacheSignedRequest() {
        $this->signedRequest = null;
    }

    public function uncache() {
        $this->user = null;
        $this->signedRequest = null;
        $this->accessToken = null;
    }

}

class FBPublicGetAccessTokenFromCode extends TransientFacebook {

    public function publicGetAccessTokenFromCode($code, $redirect_uri = null) {
        return $this->getAccessTokenFromCode($code, $redirect_uri);
    }

}

class FBPublicState extends TransientFacebook {

    const STATE = 'foo';

    protected function getPersistentData($key, $default = false) {
        if ($key === 'state') {
            return self::STATE;
        }
        return parent::getPersistentData($key, $default);
    }

    public function publicGetState() {
        return $this->state;
    }

}
